<?php
/**
 * XGPHP 轻量级PHP框架
 * @link http://xgphp.xg3.cn
 * @version 2.0.0
 * @license http://www.apache.org/licenses/LICENSE-2.0
 * @author 讯高科技 <xungaokeji@qq.com>
*/
namespace xg\database;
use \xg\facade\Log;
use \xg\App;
use \xg\Exception;
use \helper\File;
use \helper\Date;
use \helper\Arr;

class Mysqli implements \xg\interfaces\Database {
	protected $selectSql = 'SELECT %DISTINCT% %EXTRA% %FIELD% FROM %TABLE% %FORCE% %JOIN% %WHERE% %GROUP% %HAVING% %UNION% %ORDER% %LIMIT% %LOCK% %COMMENT%';
	protected $insertSql = 'INSERT %EXTRA% INTO %TABLE% (%FIELD%) VALUES (%VALUES%) %COMMENT%';
	protected $updateSql = 'UPDATE %EXTRA% %TABLE% SET %SET% %JOIN% %WHERE% %ORDER% %LIMIT% %LOCK% %COMMENT%';
	protected $deleteSql = 'DELETE %EXTRA% FROM %TABLE% %USING% %JOIN% %WHERE% %ORDER% %LIMIT% %LOCK% %COMMENT%';
	
	protected $where = null;
	protected $query = [];
	protected $config=[];
	protected $tableCaches=[];
	
	public function __construct($conf) {
		$this->config=$conf;
	}
	
	protected function connect() {
		$connect = new \mysqli($this->config['hostname'], $this->config['username'], $this->config['password'], $this->config['database']);
		if ($connect->connect_error) {
			throw new Exception($connect->connect_error);
		}
		return $connect;
	}
	
	public function reset() {
		$this->query=[];
		// if($this->where){
		// 	$this->where->reset();
		// }
	}
	
	protected function execute($type) {
		switch($type){
			case 'select':
				$sql = $this->selectSql;
				break;
			case 'insert':
				$sql = $this->insertSql;
				break;
			case 'update':
				$sql = $this->updateSql;
				break;
			case 'delete':
				$sql = $this->deleteSql;
				break;
		}
		
		preg_match_all('(%([A-Z]+)%)',$sql,$matches);
		for($i=0;$i<count($matches[0]);$i++){
			$method = 'get'.ucwords($matches[1][$i]);
			if(method_exists($this,$method)) {
				$sql=str_replace($matches[0][$i],$this->$method(),$sql);
			}else{
				$sql=str_replace($matches[0][$i],'',$sql);
			}
		}
		
		$params = []; // 存储参数
		$types = ''; // 存储参数类型 i:整型 d:双精度 s:字符串
		foreach($this->query['bind'] as $key=>$val){
			$sql = str_replace($key,'?',$sql);
			if(is_int($val)){
				$types.='i';
			}elseif(is_float($val)){
				$types.='d';
			}else{
				$types.='s';
			}
			$params[]=$val;
		}
		
		if(stripos($sql,'[?pk]')!==false){
			$pk=$this->tableInfo('pk');
			if(!$pk){
				throw new Exception('miss primary key');
			}
			$sql = str_ireplace('[?pk]',$pk,$sql);
		}
		$queryData=['sql'=>$sql,'params'=>$params,'query'=>$this->query,'where'=>null];
		if($this->where){
			$queryData['where']=$this->where->data();
		}
		try{
			$connect = $this->connect();
			$stmt = $connect->prepare($sql);
			if($stmt){
				if($types){
					$stmt->bind_param($types, ...$params);
				}
				$execute = $stmt->execute();
				if($stmt->error){
					throw new Exception($stmt->error);
				}
				if($type=='select'){
					$result = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
				}elseif($type=='insert'){
					$result = $connect->insert_id;
				}else{
					$result = $execute;
				}
				$stmt->close();
				$connect->close();
			}else{
				throw new Exception($connect->error);
			}
		}catch(\Exception $e){
			throw new Exception($e->getMessage(),$queryData);
		}
		if(App::debug()){
			Log::log($queryData,'sql');
		}
		return $result;
	}
	protected function getOrder(){
		if($this->query['order']){
			return 'ORDER BY '.$this->query['order'];
		}
		return '';
	}
	protected function getValues(){
		return Arr::join($this->query['values']);
	}
	protected function getSet(){
		return Arr::join($this->query['set']);
	}
	protected function getWhere(){
		if($this->where) {
			$data=$this->where->tableInfo($this->tableInfo())->sqlData();
			if($data['sql']){
				$this->query['bind']=Arr::extend($this->query['bind'],$data['bind']);
				return 'WHERE '.$data['sql'];
			}
		}
		return '';
	}
	protected function getTable(){
		if($this->query['table']){
			$table=$this->query['table'];
		}elseif($name=$this->query['name']){
			$table=$this->config['prefix'].$name;
		}
		if($this->query['alias']){
			if($alias=$this->query['alias'][$name]){
				$alias=' '.$alias;
			}elseif($alias=$this->query['alias'][$table]){
				$alias=' '.$alias;
			}
		}
		if($table){
			return $table.$alias;
		}
		throw new Exception('miss query table name',['query'=>$this->query]);
	}
	protected function getLimit(){
		if($this->query['limit']){
			return 'LIMIT '.$this->query['limit'];
		}
		return '';
	}
	protected function getField(){
		$func = $this->query['func'];
		if($func){
			return $func[0].'('.$func[1].')';
		}
		$fields = $this->query['fields'];
		if (!$fields) {
			$fields='*';
		}
		if ($fields=='*') {
			return '*';
		}
		$array = [];
		foreach($fields as $key=>$field){
			$field=trim($field,'`');
			if(!is_numeric($key)){
				$key=trim($key,'`');
				$array[]="`$key` AS `$field`";
			}elseif(stripos($field,' as ')!==false){
				$_arr=Arr::split(str_ireplace(' as ',' AS ',$field),' AS ');
				$key=trim($_arr[0],'`');
				$field=trim($_arr[1],'`');
				$array[]="`$key` AS `$field`";
			}else{
				$array[]="`$field`";
			}
		}
		return Arr::join($array);
	}
	
	public function query($query) {
		$this->query = $query;
		return $this;
	}
	public function where($where) {
		$this->where = $where;
		return $this;
	}
	
	protected function tableCachePath(string $tableName){
		$filename = $this->config['type'].'.'.$this->config['database'].'.'.$tableName.'.php';
		$cachefile = File::path('xg:runtime','database',$filename);
		return $cachefile;
	}
	
	protected function tableCache(string $tableName){
		if(!$this->tableCaches[$tableName] and $this->config['cache']){
			$this->tableCaches[$tableName] = include $this->tableCachePath($tableName);
		}
		return $this->tableCaches[$tableName];
	}
	
	protected function saveTableCache(string $tableName,array $info){
		$this->tableCaches[$tableName]=$info;
		if($this->config['cache']){
			$cachefile = $this->tableCachePath($tableName);
			$filecont = "<?php defined('XG') or exit('Access Deny!');return json_decode('".json_encode($info)."',true);?>";
			File::saveCont($cachefile,$filecont);
		}
	}
	
	public function tableInfo(string $keyName=''){
		$tableName = $this->getTable();
		if(!$info=$this->tableCache($tableName)){
			$connect = $this->connect();
			$result = $connect->query("DESCRIBE $tableName");
			$info = ['fields'=>[]];
			if ($result) {
				while ($row = $result->fetch_assoc()) {
					$name = $row["Field"];
					$type = strtolower($row['Type']);
					$key = $row["Key"];
					$extra = $row["Extra"];
					$isNull = !!$row["Null"];
					$default = $row["Default"];
					$fieldInfo = ['name'=>$name,'type'=>$type,'key'=>$key,'extra'=>$extra,'isNull'=>$isNull,'default'=>$default];
					if(strpos($type, '(') !== false && strpos($type, ')') !== false){
						$index = strpos($type, '(');
						$fieldInfo['type'] = substr($type, 0, $index);
						$startIndex = $index + 1;
						$endIndex = strpos($type, ')');
						$length = trim(substr($type, $startIndex, $endIndex - $startIndex));
						$fieldInfo['length'] = $length;
					}
					if($key=='PRI'){
						$info['pk'] = $name;
					}
					if(stripos($extra,'auto_increment')!==false){
						$info['auto_increment'] = true;
					}
					$info['fields'][$name] = $fieldInfo;
				}
				$this->saveTableCache($tableName,$info);
			}
			$connect->close();
		}
		if($keyName){
			return $info[$keyName];
		}
		return $info;
	}
	
	public function select() {
		return $this->execute('select');
	}
	public function find() {
		if(!$this->query['limit']){
			$this->query['limit']='1';
		}
		$execute=$this->execute('select');
		if($execute){
			return $execute[0];
		}else{
			return $execute;
		}
	}
	public function insert(array $data) {
		if(!is_array($this->query['bind'])){
			$this->query['bind'] = [];
		}
		if(!is_array($this->query['fields'])){
			$this->query['fields'] = [];
		}
		if(!is_array($this->query['values'])){
			$this->query['values'] = [];
		}
		$fields = $this->tableInfo('fields');
		$bindIndex=0;
		foreach($data as $key=>$val){
			$key=trim($key,' `');
			if($fields[$key]){
				switch($fields[$key]['type']){
					case 'tinyint':
					case 'smallint':
					case 'mediumint':
					case 'int':
					case 'bigint':
						$val=intval($val);
						break;
					case 'float':
						$val=floatval($val);
						break;
					case 'date':
						$val=Date::format($val,$this->query['date_format']);
						break;
					case 'time':
						$val=Date::format($val,$this->query['time_format']);
						break;
					case 'datetime':
						$val=Date::format($val,$this->query['datetime_format']);
						break;
				}
				$bindParam = '[?values:'.($bindIndex++).']';
				$this->query['bind'][$bindParam] = $val;
				$this->query['values'][] = $bindParam;
				$this->query['fields'][] = $key;
			}
		}
		return $this->execute('insert');
	}
	public function dec($fields,float $num) {
		return $this->inc($fields,-$num);
	}
	public function inc($fields,float $num) {
		if(!is_array($this->query['set'])){
			$this->query['set'] = [];
		}
		$fields=Arr::split($fields);
		foreach($fields as $val){
			$val=trim($val,'`');
			$this->query['set'][] = '`'.$val.'`=`'.$val.'` + '.$num;
		}
		return $this->execute('update');
	}
	public function update(array $data) {
		if(!is_array($this->query['bind'])){
			$this->query['bind'] = [];
		}
		if(!is_array($this->query['fields'])){
			$this->query['fields'] = [];
		}
		if(!is_array($this->query['set'])){
			$this->query['set'] = [];
		}
		$fields = $this->tableInfo('fields');
		$bindIndex=0;
		foreach($data as $key=>$val){
			$key=trim($key,' `');
			if($fields[$key]) {
				switch($fields[$key]['type']){
					case 'tinyint':
					case 'smallint':
					case 'mediumint':
					case 'int':
					case 'bigint':
						$val=intval($val);
						break;
					case 'float':
						$val=floatval($val);
						break;
					case 'date':
						$val=Date::format($val,$this->query['date_format']);
						break;
					case 'time':
						$val=Date::format($val,$this->query['time_format']);
						break;
					case 'datetime':
						$val=Date::format($val,$this->query['datetime_format']);
						break;
				}
				$bindParam = '[?set:'.($bindIndex++).']';
				$this->query['set'][] = '`'.$key.'`='.$bindParam;
				$this->query['bind'][$bindParam] = $val;
				$this->query['fields'][] = $key;
			}
		}
		return $this->execute('update');
	}
	public function delete() {
		return $this->execute('delete');
	}
}
?>